package com.wang.redisstudy.lock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Redis分布式缓存锁工具类
 * 分布式锁的实现思路：
 * 1.多个线程并发加锁请求，同一时刻只有一个线程能获取到锁
 * 2.线程使用完锁后释放锁，保证其他业务能正常获取到锁
 * 3.为锁设置一个过期时间，防止锁释放失败导致死锁
 * <p>
 *
 * @author wly
 * @date 2018-08-11 16:16
 */
@Component
public class RedisLockUtils {
    private static Logger logger = LoggerFactory.getLogger(RedisLockUtils.class);
    /**
     * 锁key的前缀
     */
    public static final String LOCK_KEY_PREFIX = "redisLock_";
    /**
     * 线程获取锁默认的重试次数
     */
    public static final int DEFAULT_LOCK_DEFAULT_RETRY_TIMES = 3;
    /**
     * 获取锁失败的休眠时间
     */
    public static final long DEFAULT_LOCK_FAILED_SLEEP_TIME_MILLIS = 500L;
    /**
     * 持有锁的默认超时时间
     */
    public static final long DEFAULT_LOCK_TIME_OUT_SECOND = 10L;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private ThreadLocal<String> lockFlag = new ThreadLocal<>();
    /**
     * 释放锁的LUA脚本
     */
    public static final String UNLOCK_LUA;

    /**
     * 初始化LUA脚本
     */
    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }

    /**
     * 获取锁
     * 获取锁成功：
     * key：LOCK_KEY_PREFIX + businessTag + businessValue，value：UUID
     *
     * @param businessTag   业务标识，redis key的一部分，如：商品传goods，订单传order
     * @param businessValue 业务值，如传1，对businessTag业务标识为1的数据进行加锁
     * @return 获取锁的结果，true：成功 false：失败
     * @author wly
     * @date 2018-08-13 09:15:00
     */
    public boolean lock(String businessTag, String businessValue) {
        if (StringUtils.isEmpty(businessTag) || StringUtils.isEmpty(businessValue)) {
            return false;
        }
        // 获取锁的次数
        int retryTimes = 1;
        return setRedis(businessTag, businessValue, retryTimes);
    }

    public boolean lock(String businessTag, String businessValue, int retryTimes){
        if (StringUtils.isEmpty(businessTag) || StringUtils.isEmpty(businessValue)) {
            return false;
        }
        // 获取锁的次数
        return setRedis(businessTag, businessValue, retryTimes);
    }

    /**
     * 获取锁
     * 获取锁成功：
     * key：LOCK_KEY_PREFIX + businessTag + businessValue，value：UUID
     *
     * @param businessTag   业务标识，redis key的一部分，如：商品传goods，订单传order
     * @param businessValue 业务值，如传1，对businessTag业务标识为1的数据进行加锁
     * @param retryTimes    线程尝试获取锁的次数
     * @return 获取锁的结果，true：成功 false：失败
     * @author wly
     * @date 2018-08-13 09:15:00
     */
    private boolean setRedis(String businessTag, String businessValue, int retryTimes) {
        boolean result = stringRedisTemplate.execute((RedisConnection redisConnection) -> {
            // 生成UUID记录当前哪个线程持有锁
            String value = UUID.randomUUID().toString();
            lockFlag.set(value);
            // redis key
            String key = LOCK_KEY_PREFIX + businessTag + businessValue;
            // 原子操作：setNx + expire
            return redisConnection.set(key.getBytes(), value.getBytes(), Expiration.seconds(DEFAULT_LOCK_TIME_OUT_SECOND), RedisStringCommands.SetOption.SET_IF_ABSENT);

        });
        // 获取锁失败，在有效次数内进行重试
        if (!result && retryTimes <= DEFAULT_LOCK_DEFAULT_RETRY_TIMES) {
            // 休眠一段时间
            try {
                TimeUnit.MILLISECONDS.sleep(DEFAULT_LOCK_FAILED_SLEEP_TIME_MILLIS);
            } catch (InterruptedException e) {
                logger.error("获取锁休眠线程中断", e);
                Thread.currentThread().interrupt();
            }
            // 递归获取锁
            return setRedis(businessTag, businessValue, ++retryTimes);
        }
        return result;
    }

    /**
     * 释放锁
     * 不能直接用del操作释放锁，因为如果业务执行时间过长（超过持有锁的时间）就会导致释放掉其他线程的锁
     * 添加锁的时候，我们为每个线程生成了一个UUID作为标识，通过LUA脚本检索到当前线程持有的锁并释放
     *
     * @param businessTag   业务标识，redis key的一部分，如：商品传goods，订单传order
     * @param businessValue 业务值，如传1，对businessTag业务标识为1的数据进行加锁
     * @return 释放锁的结果，true：成功 false：失败
     * @author wly
     * @date 2018-08-13 09:15:00
     */
    public boolean releaseLock(String businessTag, String businessValue) {
        if (StringUtils.isEmpty(businessTag) || StringUtils.isEmpty(businessValue)) {
            return false;
        }
        // key
        String key = LOCK_KEY_PREFIX + businessTag + businessValue;
        // value
        String value = lockFlag.get();
        List<String> args = new ArrayList<>();
        args.add(lockFlag.get());
        // 脚本对象
        RedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_LUA, Long.class);
        // 执行脚本
        Long result = stringRedisTemplate.execute(script, Collections.singletonList(key), value);
        lockFlag.remove();
        return result > 0;
    }

}
